/*
 * Copyright (c) 2002, 2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package com.sun.jmx.remote.internal;

import com.sun.jmx.remote.security.NotificationAccessController;
import com.sun.jmx.remote.util.ClassLogger;
import com.sun.jmx.remote.util.EnvHelp;
import java.io.IOException;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.management.InstanceNotFoundException;
import javax.management.ListenerNotFoundException;
import javax.management.MBeanPermission;
import javax.management.MBeanServer;
import javax.management.MBeanServerDelegate;
import javax.management.MBeanServerNotification;
import javax.management.Notification;
import javax.management.NotificationBroadcaster;
import javax.management.NotificationFilter;
import javax.management.ObjectInstance;
import javax.management.ObjectName;
import javax.management.remote.NotificationResult;
import javax.management.remote.TargetedNotification;
import javax.management.MalformedObjectNameException;
import javax.security.auth.Subject;

public class ServerNotifForwarder {


  public ServerNotifForwarder(MBeanServer mbeanServer,
      Map<String, ?> env,
      NotificationBuffer notifBuffer,
      String connectionId) {
    this.mbeanServer = mbeanServer;
    this.notifBuffer = notifBuffer;
    this.connectionId = connectionId;
    connectionTimeout = EnvHelp.getServerConnectionTimeout(env);

    String stringBoolean = (String) env.get("jmx.remote.x.check.notification.emission");
    checkNotificationEmission = EnvHelp.computeBooleanFromString(stringBoolean);
    notificationAccessController =
        EnvHelp.getNotificationAccessController(env);
  }

  public Integer addNotificationListener(final ObjectName name,
      final NotificationFilter filter)
      throws InstanceNotFoundException, IOException {

    if (logger.traceOn()) {
      logger.trace("addNotificationListener",
          "Add a listener at " + name);
    }

    checkState();

    // Explicitly check MBeanPermission for addNotificationListener
    //
    checkMBeanPermission(name, "addNotificationListener");
    if (notificationAccessController != null) {
      notificationAccessController.addNotificationListener(
          connectionId, name, getSubject());
    }
    try {
      boolean instanceOf =
          AccessController.doPrivileged(
              new PrivilegedExceptionAction<Boolean>() {
                public Boolean run() throws InstanceNotFoundException {
                  return mbeanServer.isInstanceOf(name, broadcasterClass);
                }
              });
      if (!instanceOf) {
        throw new IllegalArgumentException("The specified MBean [" +
            name + "] is not a " +
            "NotificationBroadcaster " +
            "object.");
      }
    } catch (PrivilegedActionException e) {
      throw (InstanceNotFoundException) extractException(e);
    }

    final Integer id = getListenerID();

    // 6238731: set the default domain if no domain is set.
    ObjectName nn = name;
    if (name.getDomain() == null || name.getDomain().equals("")) {
      try {
        nn = ObjectName.getInstance(mbeanServer.getDefaultDomain(),
            name.getKeyPropertyList());
      } catch (MalformedObjectNameException mfoe) {
        // impossible, but...
        IOException ioe = new IOException(mfoe.getMessage());
        ioe.initCause(mfoe);
        throw ioe;
      }
    }

    synchronized (listenerMap) {
      IdAndFilter idaf = new IdAndFilter(id, filter);
      Set<IdAndFilter> set = listenerMap.get(nn);
      // Tread carefully because if set.size() == 1 it may be the
      // Collections.singleton we make here, which is unmodifiable.
      if (set == null) {
        set = Collections.singleton(idaf);
      } else {
        if (set.size() == 1) {
          set = new HashSet<IdAndFilter>(set);
        }
        set.add(idaf);
      }
      listenerMap.put(nn, set);
    }

    return id;
  }

  public void removeNotificationListener(ObjectName name,
      Integer[] listenerIDs)
      throws Exception {

    if (logger.traceOn()) {
      logger.trace("removeNotificationListener",
          "Remove some listeners from " + name);
    }

    checkState();

    // Explicitly check MBeanPermission for removeNotificationListener
    //
    checkMBeanPermission(name, "removeNotificationListener");
    if (notificationAccessController != null) {
      notificationAccessController.removeNotificationListener(
          connectionId, name, getSubject());
    }

    Exception re = null;
    for (int i = 0; i < listenerIDs.length; i++) {
      try {
        removeNotificationListener(name, listenerIDs[i]);
      } catch (Exception e) {
        // Give back the first exception
        //
        if (re != null) {
          re = e;
        }
      }
    }
    if (re != null) {
      throw re;
    }
  }

  public void removeNotificationListener(ObjectName name, Integer listenerID)
      throws
      InstanceNotFoundException,
      ListenerNotFoundException,
      IOException {

    if (logger.traceOn()) {
      logger.trace("removeNotificationListener",
          "Remove the listener " + listenerID + " from " + name);
    }

    checkState();

    if (name != null && !name.isPattern()) {
      if (!mbeanServer.isRegistered(name)) {
        throw new InstanceNotFoundException("The MBean " + name +
            " is not registered.");
      }
    }

    synchronized (listenerMap) {
      // Tread carefully because if set.size() == 1 it may be a
      // Collections.singleton, which is unmodifiable.
      Set<IdAndFilter> set = listenerMap.get(name);
      IdAndFilter idaf = new IdAndFilter(listenerID, null);
      if (set == null || !set.contains(idaf)) {
        throw new ListenerNotFoundException("Listener not found");
      }
      if (set.size() == 1) {
        listenerMap.remove(name);
      } else {
        set.remove(idaf);
      }
    }
  }

  /* This is the object that will apply our filtering to candidate
   * notifications.  First of all, if there are no listeners for the
   * ObjectName that the notification is coming from, we go no further.
   * Then, for each listener, we must apply the corresponding filter (if any)
   * and ignore the listener if the filter rejects.  Finally, we apply
   * some access checks which may also reject the listener.
   *
   * A given notification may trigger several listeners on the same MBean,
   * which is why listenerMap is a Map<ObjectName, Set<IdAndFilter>> and
   * why we add the found notifications to a supplied List rather than
   * just returning a boolean.
   */
  private final NotifForwarderBufferFilter bufferFilter = new NotifForwarderBufferFilter();

  final class NotifForwarderBufferFilter implements NotificationBufferFilter {

    public void apply(List<TargetedNotification> targetedNotifs,
        ObjectName source, Notification notif) {
      // We proceed in two stages here, to avoid holding the listenerMap
      // lock while invoking the filters (which are user code).
      final IdAndFilter[] candidates;
      synchronized (listenerMap) {
        final Set<IdAndFilter> set = listenerMap.get(source);
        if (set == null) {
          logger.debug("bufferFilter", "no listeners for this name");
          return;
        }
        candidates = new IdAndFilter[set.size()];
        set.toArray(candidates);
      }
      // We don't synchronize on targetedNotifs, because it is a local
      // variable of our caller and no other thread can see it.
      for (IdAndFilter idaf : candidates) {
        final NotificationFilter nf = idaf.getFilter();
        if (nf == null || nf.isNotificationEnabled(notif)) {
          logger.debug("bufferFilter", "filter matches");
          final TargetedNotification tn =
              new TargetedNotification(notif, idaf.getId());
          if (allowNotificationEmission(source, tn)) {
            targetedNotifs.add(tn);
          }
        }
      }
    }
  }

  ;

  public NotificationResult fetchNotifs(long startSequenceNumber,
      long timeout,
      int maxNotifications) {
    if (logger.traceOn()) {
      logger.trace("fetchNotifs", "Fetching notifications, the " +
          "startSequenceNumber is " + startSequenceNumber +
          ", the timeout is " + timeout +
          ", the maxNotifications is " + maxNotifications);
    }

    NotificationResult nr;
    final long t = Math.min(connectionTimeout, timeout);
    try {
      nr = notifBuffer.fetchNotifications(bufferFilter,
          startSequenceNumber,
          t, maxNotifications);
      snoopOnUnregister(nr);
    } catch (InterruptedException ire) {
      nr = new NotificationResult(0L, 0L, new TargetedNotification[0]);
    }

    if (logger.traceOn()) {
      logger.trace("fetchNotifs", "Forwarding the notifs: " + nr);
    }

    return nr;
  }

  // The standard RMI connector client will register a listener on the MBeanServerDelegate
  // in order to be told when MBeans are unregistered.  We snoop on fetched notifications
  // so that we can know too, and remove the corresponding entry from the listenerMap.
  // See 6957378.
  private void snoopOnUnregister(NotificationResult nr) {
    List<IdAndFilter> copy = null;
    synchronized (listenerMap) {
      Set<IdAndFilter> delegateSet = listenerMap.get(MBeanServerDelegate.DELEGATE_NAME);
      if (delegateSet == null || delegateSet.isEmpty()) {
        return;
      }
      copy = new ArrayList<>(delegateSet);
    }

    for (TargetedNotification tn : nr.getTargetedNotifications()) {
      Integer id = tn.getListenerID();
      for (IdAndFilter idaf : copy) {
        if (idaf.id == id) {
          // This is a notification from the MBeanServerDelegate.
          Notification n = tn.getNotification();
          if (n instanceof MBeanServerNotification &&
              n.getType().equals(MBeanServerNotification.UNREGISTRATION_NOTIFICATION)) {
            MBeanServerNotification mbsn = (MBeanServerNotification) n;
            ObjectName gone = mbsn.getMBeanName();
            synchronized (listenerMap) {
              listenerMap.remove(gone);
            }
          }
        }
      }
    }
  }

  public void terminate() {
    if (logger.traceOn()) {
      logger.trace("terminate", "Be called.");
    }

    synchronized (terminationLock) {
      if (terminated) {
        return;
      }

      terminated = true;

      synchronized (listenerMap) {
        listenerMap.clear();
      }
    }

    if (logger.traceOn()) {
      logger.trace("terminate", "Terminated.");
    }
  }

  //----------------
  // PRIVATE METHODS
  //----------------

  private Subject getSubject() {
    return Subject.getSubject(AccessController.getContext());
  }

  private void checkState() throws IOException {
    synchronized (terminationLock) {
      if (terminated) {
        throw new IOException("The connection has been terminated.");
      }
    }
  }

  private Integer getListenerID() {
    synchronized (listenerCounterLock) {
      return listenerCounter++;
    }
  }

  /**
   * Explicitly check the MBeanPermission for
   * the current access control context.
   */
  public final void checkMBeanPermission(
      final ObjectName name, final String actions)
      throws InstanceNotFoundException, SecurityException {
    checkMBeanPermission(mbeanServer, name, actions);
  }

  static void checkMBeanPermission(
      final MBeanServer mbs, final ObjectName name, final String actions)
      throws InstanceNotFoundException, SecurityException {

    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
      AccessControlContext acc = AccessController.getContext();
      ObjectInstance oi;
      try {
        oi = AccessController.doPrivileged(
            new PrivilegedExceptionAction<ObjectInstance>() {
              public ObjectInstance run()
                  throws InstanceNotFoundException {
                return mbs.getObjectInstance(name);
              }
            });
      } catch (PrivilegedActionException e) {
        throw (InstanceNotFoundException) extractException(e);
      }
      String classname = oi.getClassName();
      MBeanPermission perm = new MBeanPermission(
          classname,
          null,
          name,
          actions);
      sm.checkPermission(perm, acc);
    }
  }

  /**
   * Check if the caller has the right to get the following notifications.
   */
  private boolean allowNotificationEmission(ObjectName name,
      TargetedNotification tn) {
    try {
      if (checkNotificationEmission) {
        checkMBeanPermission(name, "addNotificationListener");
      }
      if (notificationAccessController != null) {
        notificationAccessController.fetchNotification(
            connectionId, name, tn.getNotification(), getSubject());
      }
      return true;
    } catch (SecurityException e) {
      if (logger.debugOn()) {
        logger.debug("fetchNotifs", "Notification " +
            tn.getNotification() + " not forwarded: the " +
            "caller didn't have the required access rights");
      }
      return false;
    } catch (Exception e) {
      if (logger.debugOn()) {
        logger.debug("fetchNotifs", "Notification " +
            tn.getNotification() + " not forwarded: " +
            "got an unexpected exception: " + e);
      }
      return false;
    }
  }

  /**
   * Iterate until we extract the real exception
   * from a stack of PrivilegedActionExceptions.
   */
  private static Exception extractException(Exception e) {
    while (e instanceof PrivilegedActionException) {
      e = ((PrivilegedActionException) e).getException();
    }
    return e;
  }

  private static class IdAndFilter {

    private Integer id;
    private NotificationFilter filter;

    IdAndFilter(Integer id, NotificationFilter filter) {
      this.id = id;
      this.filter = filter;
    }

    Integer getId() {
      return this.id;
    }

    NotificationFilter getFilter() {
      return this.filter;
    }

    @Override
    public int hashCode() {
      return id.hashCode();
    }

    @Override
    public boolean equals(Object o) {
      return ((o instanceof IdAndFilter) &&
          ((IdAndFilter) o).getId().equals(getId()));
    }
  }

  //------------------
  // PRIVATE VARIABLES
  //------------------

  private MBeanServer mbeanServer;

  private final String connectionId;

  private final long connectionTimeout;

  private static int listenerCounter = 0;
  private final static int[] listenerCounterLock = new int[0];

  private NotificationBuffer notifBuffer;
  private final Map<ObjectName, Set<IdAndFilter>> listenerMap =
      new HashMap<ObjectName, Set<IdAndFilter>>();

  private boolean terminated = false;
  private final int[] terminationLock = new int[0];

  static final String broadcasterClass =
      NotificationBroadcaster.class.getName();

  private final boolean checkNotificationEmission;

  private final NotificationAccessController notificationAccessController;

  private static final ClassLogger logger =
      new ClassLogger("javax.management.remote.misc", "ServerNotifForwarder");
}
